﻿/****************************** Module Header ******************************\
Module Name:    NewMessageTemplateFileCommand.cs
Project:        SQL Server Error Log Parsing module

This is the implementation of the New-MessageTemplateFile Cmdlet, which 
post-processes a list of SQL Server system messages and converts the 
printf() style tokens into Regular Expression patterns, to be used when 
parsing SQL Server error logs.

This source is subject to the Microsoft Public License.
See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
All other rights reserved.

THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\***************************************************************************/

using System;
using System.IO;
using System.Management.Automation;
using System.Text;
using System.Text.RegularExpressions;

namespace SQLServerSleuth.PowerShell.Commands.ParseERRORLOG
{
    /// <summary>
    /// The implementation of the New-MessageTemplateFile Cmdlet.
    /// </summary>
    [Cmdlet("New", "MessageTemplateFile")]
    public class NewMessageTemplateFileCommand : PSCmdlet
    {
        #region Cmdlet parameters

        /// <summary>
        /// The path to the file into which the targeted SQL Server system 
        /// messages have been exported (via the Export-SystemMessage 
        /// Cmdlet).
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
        public string SystemMessageFilePath
        {
            get
            {
                return this._systemMessageFilePath;
            }

            set
            {
                this._systemMessageFilePath = value.Trim();
            }
        }
        public string _systemMessageFilePath = "";

        /// <summary>
        /// The path to the text file into which the message templates 
        /// should be written.
        /// </summary>
        [Parameter(Mandatory = true, Position = 1)]
        public string OutputFilePath
        {
            get
            {
                return this._outputFilePath;
            }

            set
            {
                this._outputFilePath = value.Trim();
            }
        }
        public string _outputFilePath = "";

        #endregion Cmdlet parameters

        #region Overridden methods from the System.Management.Automation.PSCmdlet class

        /// <summary>
        /// Post-process each of the system messages in the specified file.
        /// </summary>
        protected override void ProcessRecord()
        {
            //  Write the values of the input parameters to the debug stream.
            this.writeDebugOutput_CmdletParameters();

            int highestTokenCount = 0;
            string[] inputFields = null;
            string line = "";
            int lineCounter = -1;
            string messageTemplate = "";
            int messageTemplateCounter = 0;
            StringBuilder output = new StringBuilder();
            Regex regexSQLServerFormatString = new Regex
            (
                @"%(?<Parameter>\d+\$)?(?<Flags>[\d\+\-#]+)?(?<Width>\d+|\*)?(?<Precision>\.(?:\d+|\*))?(?<Length>(?:I32|I64|I|L|hh|h|j|ll|l|q|t|z))?(?<Type>(?:S_[A-Z]+|E|F|G|X|c|d|e|f|g|i|n|o|p|s|u|x|\d(?:\!\w+\!)|\d))",
                RegexOptions.IgnoreCase | RegexOptions.Singleline
            );
            Regex regexToken = new Regex
            (
                @"__TOKEN__",
                RegexOptions.IgnoreCase | RegexOptions.Singleline
            );
            int tokenCount = 0;
            const string tokenString = "__TOKEN__";

            //  Verify that the output directory already exists.
            this._outputDirectoryPath = this.getOutputDirectoryPath(this._outputFilePath);

            //  Verify that the system messages file exists.
            if (!File.Exists(this._systemMessageFilePath))
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    new System.IO.FileNotFoundException("outputDirectoryPath"),
                    "InvalidSystemMessageFilePath",
                    ErrorCategory.ObjectNotFound,
                    "System messages input file"
                );
                ThrowTerminatingError(errorRecord);
            }

            try
            {
                using
                (
                    StreamReader systemMessagesStream = new StreamReader
                    (
                        this._systemMessageFilePath,
                        true
                    )
                )
                {
                    while (null != (line = systemMessagesStream.ReadLine()))
                    {
                        lineCounter++;
                        if (0 < line.Trim().Length)
                        {
                            messageTemplateCounter++;
                            
                            //  Validate that the expected columns are 
                            //  present in the current line.
                            if (regEx_SystemMessageLine.IsMatch(line))
                            {
                                inputFields = line.Trim().Split('\t');

                                //  Process the system message text:
                                //      1.  Replace the printf() style 
                                //          tokens with the string 
                                //          "__TOKEN__".
                                //      2.  Then use the 
                                //          System.Text.RegularExpressions.Regex.Escape() 
                                //          static method to escape any 
                                //          remaining characters that would 
                                //          appear to be metacharacters.
                                //      3.  Unescape spaces to reduce the 
                                //          size of the output file.
                                //      4.  Replace any percent signs that 
                                //          were escaped for the printf() 
                                //          style tokens.
                                messageTemplate = Regex.Escape
                                (
                                    regexSQLServerFormatString.Replace
                                    (
                                        inputFields[(int)inputFieldNumbers.MessageText],
                                        tokenString
                                    )
                                ).Replace(
                                    "\\ ",
                                    " "
                                ).Replace(
                                    "%%",
                                    "%"
                                );

                                //  Count the number of tokens in the current 
                                //  message template.
                                tokenCount = regexToken.Matches(messageTemplate).Count;
                                if (tokenCount > highestTokenCount)
                                {
                                    highestTokenCount = tokenCount;
                                }

                                //  Add the current message template to the 
                                //  output.  In the process, replace the 
                                //  "__TOKEN__" string with the appropriate 
                                //  Regular Expression metacharacters.
                                output.AppendFormat
                                (
                                    "{0}\t{1}\t{2}\t{3}\t{4}\r\n",
                                    inputFields[(int)inputFieldNumbers.MessageID],
                                    inputFields[(int)inputFieldNumbers.Severity],
                                    inputFields[(int)inputFieldNumbers.IsLogged],
                                    tokenCount,
                                    messageTemplate.Replace
                                    (
                                        tokenString,
                                        "(.*?)"
                                    )
                                );
                            }
                            else
                            {
                                //  Create a new error record, populate all 
                                //  relevant properties, and then write it 
                                //  to the error stream.
                                ErrorRecord errorRecord = new ErrorRecord
                                (
                                    new System.IO.FileFormatException
                                    (
                                        string.Format
                                        (
                                            "Line {0} of the \"{1}\" system messages file is not in the expected format.",
                                            lineCounter,
                                            this._systemMessageFilePath
                                        )
                                    ),
                                    "ParseError",
                                    ErrorCategory.ReadError,
                                    "SystemMessageFile"
                                );
                                WriteError(errorRecord);
                            }
                        }
                    }
                }
                WriteDebug
                (
                    string.Format
                    (
                        "Highest token count = {0}",
                        highestTokenCount
                    )
                );
                WriteDebug
                (
                    string.Format
                    (
                        "Message template count = {0}",
                        messageTemplateCounter
                    )
                );

                //  Write the output to file.
                System.IO.File.WriteAllText
                (
                    this._outputFilePath,
                    output.ToString(),
                    System.Text.Encoding.Unicode
                );

                //  Write a status message to the verbose stream.
                if (1 == messageTemplateCounter)
                {
                    WriteVerbose
                    (
                        string.Format
                        (
                            "{0:N0} message template was written to the \"{1}\" file.",
                            messageTemplateCounter,
                            this._outputFilePath
                        )
                    );
                }
                else
                {
                    WriteVerbose
                    (
                        string.Format
                        (
                            "{0:N0} message templates were written to the \"{1}\" file.",
                            messageTemplateCounter,
                            this._outputFilePath
                        )
                    );
                }

                //  Write the name of the output file to the output pipeline.
                WriteObject(this._outputFilePath);
            }
            catch (Exception exception)
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    exception,
                    "ListCreationFailure",
                    ErrorCategory.WriteError,
                    "Message templates list"
                );
                ThrowTerminatingError(errorRecord);
            }

            return;
        }

        #endregion Overridden methods from the System.Management.Automation.PSCmdlet class

        #region Private class members

        /// <summary>
        /// The ordinal values for the columns that are parsed from the 
        /// input file.
        /// </summary>
        private enum inputFieldNumbers
        {
            MessageID = 0,
            Severity = 1,
            IsLogged = 2,
            MessageText = 3
        };

        /// <summary>
        /// A constant Regular Expression object that is used to validate 
        /// the lines loaded from the system messages file.
        /// </summary>
        private static readonly Regex regEx_SystemMessageLine = new Regex(
            @"^(?<MessageID>[\d-]+)\t(?<Severity>\d+)\t(?<IsLogged>\d)\t(?<SystemMessage>.+)$",
            RegexOptions.IgnoreCase
        );

        private string _outputDirectoryPath = "";

        /// <summary>
        /// Extract the directory path from the output file path and insure 
        /// that it corresponds to an existing directory.
        /// </summary>
        /// <param name="outputFilePath">The path to the output file for the Cmdlet.</param>
        /// <returns>The path to the directory into which the output file 
        /// should be written.</returns>
        private string getOutputDirectoryPath(string outputFilePath)
        {
            string outputDirectoryPath = "";

            //  Verify that the given output file path doesn't refer to an 
            //  existing directory (which is unusual but possible).
            if (Directory.Exists(outputFilePath))
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    new InvalidOperationException
                    (
                        string.Format
                        (
                            "The specified output file path \"{0}\" corresponds to an existing directory.",
                            outputFilePath
                        )
                    ),
                    "InvalidOutputFilePath",
                    ErrorCategory.InvalidArgument,
                    "System messages output file"
                );
                ThrowTerminatingError(errorRecord);
            }

            //  Extract the path to the parent directory.
            outputDirectoryPath = Path.GetDirectoryName(outputFilePath);

            //  Verify that the parent directory already exists.
            if (!Directory.Exists(outputDirectoryPath))
            {
                ErrorRecord errorRecord = new ErrorRecord
                (
                    new System.IO.DirectoryNotFoundException("outputDirectoryPath"),
                    "InvalidOutputFilePath",
                    ErrorCategory.ObjectNotFound,
                    "System messages output file"
                );
                ThrowTerminatingError(errorRecord);
            }

            return outputDirectoryPath;
        }

        /// <summary>
        /// Write the values of all relevant Cmdlet parameters and arguments 
        /// to the debug stream.
        /// </summary>
        private void writeDebugOutput_CmdletParameters()
        {
            WriteDebug
            (
                string.Format
                (
                    "Parameter set = {0}",
                    this.ParameterSetName
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$SystemMessageFilePath = \"{0}\"",
                    this._systemMessageFilePath
                )
            );
            WriteDebug
            (
                string.Format
                (
                    "$OutputFilePath = \"{0}\"",
                    this._outputFilePath
                )
            );

            return;
        }
        #endregion Private class members
    }
}
